Popular Searches
Popular Course Categories
Popular Courses

Building dynamic lists efficiently

Building dynamic lists efficiently

Flutter Lists & Collections

Building Dynamic Lists Efficiently in Flutter

Dynamic lists are an essential part of Flutter applications because real-world applications frequently display changing data such as products, users, messages, notifications, orders, search results, and API responses. Flutter provides widgets such as ListView.builder, ListView.separated, and GridView.builder to create lists efficiently.

For large or dynamically changing collections, ListView.builder is especially useful because list items are created on demand as they become visible instead of creating every item at once.

1. What Is a Dynamic List?

A dynamic list is a list whose contents can change during application execution. Items may be added, removed, updated, filtered, sorted, or loaded from an API or database.

Examples of Dynamic Lists

  • Product lists
  • User lists
  • Chat messages
  • Notifications
  • Shopping cart items
  • Search results
  • News feeds
  • Orders and transactions
  • Social media posts
  • API-generated data

2. Why Efficient List Building Is Important

Suppose an application contains 10,000 products. Creating all 10,000 widgets immediately can require unnecessary memory and layout work. Flutter's lazy builder widgets allow the application to create only the widgets needed for the current viewport and scrolling range.

Flutter's official documentation recommends lazy builder methods for large lists and grids because only the visible portion needs to be built initially. Flutter Performance Best Practices

3. ListView vs ListView.builder

ListView

The normal ListView constructor receives an explicit list of widgets. It is convenient for small, fixed lists.

ListView(
  children: const [
    ListTile(title: Text('Apple')),
    ListTile(title: Text('Banana')),
    ListTile(title: Text('Orange')),
  ],
)

ListView.builder

ListView.builder creates list items on demand through the itemBuilder callback. It is appropriate for large or dynamically generated lists.

ListView.builder(
  itemCount: products.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(products[index]),
    );
  },
)

Flutter's API documentation states that ListView.builder creates a scrollable linear array of widgets on demand and is appropriate for large or potentially infinite collections. ListView.builder API Documentation

4. Basic Syntax of ListView.builder

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return WidgetName();
  },
)

Important Properties

PropertyPurpose
itemCountSpecifies the number of items.
itemBuilderCreates each list item.
scrollDirectionControls vertical or horizontal scrolling.
paddingAdds space around the list.
controllerControls and observes scrolling.
physicsControls scrolling behavior.
shrinkWrapMakes the list size itself according to its content when required by the layout.
itemExtentProvides a fixed extent for every item.
prototypeItemUses a prototype widget to determine a common item extent.
itemExtentBuilderProvides item extents dynamically for different-sized items.
findChildIndexCallbackHelps preserve child state when item order changes.

5. Creating a Dynamic List from a Dart List

import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(
    home: ProductScreen(),
  ));
}

class ProductScreen extends StatelessWidget {
  const ProductScreen({super.key});

  final List products = const [
    'Laptop',
    'Mobile Phone',
    'Tablet',
    'Headphones',
    'Keyboard',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Products'),
      ),
      body: ListView.builder(
        itemCount: products.length,
        itemBuilder: (context, index) {
          return ListTile(
            leading: const Icon(Icons.shopping_bag),
            title: Text(products[index]),
          );
        },
      ),
    );
  }
}

6. How itemBuilder Works

The itemBuilder callback receives two important values:

  • context - The current widget build context.
  • index - The position of the item being created.
itemBuilder: (context, index) {
  return ListTile(
    title: Text('Item $index'),
  );
}

For example, when index is 0, the first item is created. When it is 1, the second item is created, and so on.

7. Using itemCount Correctly

When the number of items is known, provide itemCount.

ListView.builder(
  itemCount: products.length,
  itemBuilder: (context, index) {
    return Text(products[index]);
  },
)

Providing a non-null itemCount helps Flutter estimate the maximum scroll extent more accurately. ListView.builder API

8. Building Lists from Objects

In real applications, lists usually contain model objects rather than simple strings.

class Product {
  final String name;
  final double price;

  Product({
    required this.name,
    required this.price,
  });
}

final products = [
  Product(name: 'Laptop', price: 55000),
  Product(name: 'Phone', price: 30000),
  Product(name: 'Tablet', price: 22000),
];

The objects can then be displayed dynamically.

ListView.builder(
  itemCount: products.length,
  itemBuilder: (context, index) {
    final product = products[index];

    return ListTile(
      title: Text(product.name),
      subtitle: Text('₹${product.price}'),
    );
  },
)

9. Dynamic Cards

Instead of a simple ListTile, you can create reusable cards.

ListView.builder(
  padding: const EdgeInsets.all(16),
  itemCount: products.length,
  itemBuilder: (context, index) {
    final product = products[index];

    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: ListTile(
        leading: const CircleAvatar(
          child: Icon(Icons.shopping_cart),
        ),
        title: Text(product.name),
        subtitle: Text('₹${product.price}'),
        trailing: const Icon(Icons.arrow_forward_ios),
      ),
    );
  },
)

10. Adding Items Dynamically

A StatefulWidget can be used when the list needs to change while the application is running.

import 'package:flutter/material.dart';

class DynamicListScreen extends StatefulWidget {
  const DynamicListScreen({super.key});

  @override
  State createState() => _DynamicListScreenState();
}

class _DynamicListScreenState extends State {
  final List items = [
    'Item 1',
    'Item 2',
    'Item 3',
  ];

  void addItem() {
    setState(() {
      items.add('Item ${items.length + 1}');
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Dynamic List'),
        actions: [
          IconButton(
            onPressed: addItem,
            icon: const Icon(Icons.add),
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(items[index]),
          );
        },
      ),
    );
  }
}

11. Removing Items Dynamically

void removeItem(int index) {
  setState(() {
    items.removeAt(index);
  });
}

Use it inside the builder:

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index]),
      trailing: IconButton(
        icon: const Icon(Icons.delete),
        onPressed: () {
          removeItem(index);
        },
      ),
    );
  },
)

12. Swipe-to-Delete with Dismissible

The Dismissible widget can provide swipe-to-delete behavior.

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return Dismissible(
      key: ValueKey(items[index]),
      onDismissed: (direction) {
        setState(() {
          items.removeAt(index);
        });
      },
      background: Container(
        color: Colors.red,
        alignment: Alignment.centerRight,
        padding: const EdgeInsets.only(right: 20),
        child: const Icon(
          Icons.delete,
          color: Colors.white,
        ),
      ),
      child: ListTile(
        title: Text(items[index]),
      ),
    );
  },
)

13. Efficient Lists with ListView.separated

When list items need separators, ListView.separated can build both the items and separators lazily.

ListView.separated(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index]),
    );
  },
  separatorBuilder: (context, index) {
    return const Divider();
  },
)

The official API documentation describes ListView.separated as a fixed-length scrollable list whose items and separators are built on demand. ListView.separated API

14. Large Lists and Lazy Building

Consider a list containing 10,000 products:

final products = List.generate(
  10000,
  (index) => 'Product $index',
);

Display them efficiently:

ListView.builder(
  itemCount: products.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(products[index]),
    );
  },
)

The important point is that the application does not need to construct all 10,000 list-item widgets at the same time. Flutter's builder mechanism creates children on demand. Flutter: Work with long lists

15. Using itemExtent for Fixed-Height Items

If every item has the same height, itemExtent can provide Flutter with that information.

ListView.builder(
  itemCount: items.length,
  itemExtent: 60,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index]),
    );
  },
)

Knowing the extent of children can reduce work needed by the scrolling machinery. ListView.builder API

16. Using prototypeItem

If items have a common size but you do not want to specify a numeric extent, you can use prototypeItem.

ListView.builder(
  itemCount: items.length,
  prototypeItem: const ListTile(
    title: Text('Sample Item'),
  ),
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index]),
    );
  },
)

Flutter's long-list recipe demonstrates prototypeItem for providing extent information to the scrolling system. Work with long lists

17. itemExtentBuilder for Variable Item Sizes

When item sizes differ but their extents can be determined from the index, itemExtentBuilder can be used.

ListView.builder(
  itemCount: items.length,
  itemExtentBuilder: (index, dimensions) {
    return index.isEven ? 60 : 100;
  },
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index]),
    );
  },
)

You should use only one of itemExtent, prototypeItem, or itemExtentBuilder in a single ListView.builder. ListView.builder API

18. Horizontal Dynamic Lists

Dynamic lists do not have to scroll vertically. Use Axis.horizontal for horizontal lists.

ListView.builder(
  scrollDirection: Axis.horizontal,
  itemCount: products.length,
  itemBuilder: (context, index) {
    return Container(
      width: 180,
      margin: const EdgeInsets.all(8),
      child: Card(
        child: Center(
          child: Text(products[index]),
        ),
      ),
    );
  },
)

19. Building a List from API Data

A common application architecture is:

  1. Request data from an API.
  2. Decode JSON.
  3. Convert JSON into model objects.
  4. Store the objects in a list.
  5. Use ListView.builder to display them.
class User {
  final String name;
  final String email;

  User({
    required this.name,
    required this.email,
  });

  factory User.fromJson(Map json) {
    return User(
      name: json['name'],
      email: json['email'],
    );
  }
}

Display the API-generated list:

ListView.builder(
  itemCount: users.length,
  itemBuilder: (context, index) {
    final user = users[index];

    return ListTile(
      leading: const CircleAvatar(
        child: Icon(Icons.person),
      ),
      title: Text(user.name),
      subtitle: Text(user.email),
    );
  },
)

20. Loading, Success, Empty, and Error States

A dynamic list should handle more than just the successful data state.

if (isLoading) {
  return const Center(
    child: CircularProgressIndicator(),
  );
}

if (hasError) {
  return const Center(
    child: Text('Something went wrong'),
  );
}

if (items.isEmpty) {
  return const Center(
    child: Text('No items available'),
  );
}

return ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index]),
    );
  },
);

Flutter's ListView documentation also demonstrates conditionally replacing an empty ListView with an empty-state widget when there are no items. ListView documentation

21. Searchable Dynamic List

Search functionality usually works by filtering the source data and rebuilding the visible list.

List allProducts = [
  'Laptop',
  'Phone',
  'Tablet',
  'Keyboard',
  'Mouse',
];

String searchText = '';

List get filteredProducts {
  return allProducts
      .where(
        (product) => product
            .toLowerCase()
            .contains(searchText.toLowerCase()),
      )
      .toList();
}

Use the filtered list:

ListView.builder(
  itemCount: filteredProducts.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(filteredProducts[index]),
    );
  },
)

22. Avoid Rebuilding Unnecessary Widgets

When a list changes, keep the widget tree as simple as practical. Extract complex list items into separate widgets when appropriate.

class ProductTile extends StatelessWidget {
  final String name;
  final double price;

  const ProductTile({
    super.key,
    required this.name,
    required this.price,
  });

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(name),
      subtitle: Text('₹$price'),
    );
  }
}

Then:

ListView.builder(
  itemCount: products.length,
  itemBuilder: (context, index) {
    final product = products[index];

    return ProductTile(
      name: product.name,
      price: product.price,
    );
  },
)

23. Avoid Creating the Entire Widget List in Advance

For a very large or dynamically generated collection, avoid unnecessarily creating thousands of widgets before displaying the list.

Less suitable for a large collection:

final widgets = products.map(
  (product) => ListTile(
    title: Text(product.name),
  ),
).toList();

ListView(
  children: widgets,
)

Prefer lazy construction:

ListView.builder(
  itemCount: products.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(products[index].name),
    );
  },
)

24. Using Keys in Dynamic Lists

Keys help Flutter identify widgets when items are inserted, removed, or reordered.

ListView.builder(
  itemCount: products.length,
  itemBuilder: (context, index) {
    final product = products[index];

    return ProductTile(
      key: ValueKey(product.id),
      name: product.name,
      price: product.price,
    );
  },
)

Use a stable and unique identifier for the key when list items have persistent identity.

25. Preserving State When List Order Changes

If the order of dynamically built children can change, Flutter provides findChildIndexCallback on ListView.builder. This can help Flutter map a child back to its existing render object and preserve state when the order changes.

ListView.builder(
  itemCount: products.length,
  findChildIndexCallback: (Key key) {
    final valueKey = key as ValueKey;
    final id = valueKey.value;

    final index = products.indexWhere(
      (product) => product.id == id,
    );

    return index == -1 ? null : index;
  },
  itemBuilder: (context, index) {
    final product = products[index];

    return ProductTile(
      key: ValueKey(product.id),
      name: product.name,
      price: product.price,
    );
  },
)

This is particularly relevant when list ordering changes and child widgets contain state. ListView.builder API

26. Using ListView Inside a Column

A common layout problem occurs when a scrollable list is placed directly inside a Column.

Use Expanded when the list should occupy the remaining available space.

Column(
  children: [
    const Text(
      'Products',
      style: TextStyle(fontSize: 24),
    ),
    Expanded(
      child: ListView.builder(
        itemCount: products.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(products[index]),
          );
        },
      ),
    ),
  ],
)

27. Using shrinkWrap Carefully

shrinkWrap: true tells the scroll view to size itself based on its contents along the scroll direction. It can be useful when nesting a list inside another layout, but it can add layout work, so it should not be enabled unnecessarily.

ListView.builder(
  shrinkWrap: true,
  physics: const NeverScrollableScrollPhysics(),
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(items[index]),
    );
  },
)

For a screen that contains a single large list, prefer allowing that list to be the primary scrollable rather than unnecessarily nesting scrollable lists.

28. Dynamic Lists with Selection

class SelectionScreen extends StatefulWidget {
  const SelectionScreen({super.key});

  @override
  State createState() => _SelectionScreenState();
}

class _SelectionScreenState extends State {
  final List items = [
    'Flutter',
    'Dart',
    'Firebase',
    'Android',
  ];

  int? selectedIndex;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text(items[index]),
          selected: selectedIndex == index,
          onTap: () {
            setState(() {
              selectedIndex = index;
            });
          },
        );
      },
    );
  }
}

29. Checkbox List

class TodoScreen extends StatefulWidget {
  const TodoScreen({super.key});

  @override
  State createState() => _TodoScreenState();
}

class _TodoScreenState extends State {
  final List tasks = [
    'Learn Dart',
    'Learn Flutter',
    'Build a project',
  ];

  final List completed = [
    false,
    false,
    false,
  ];

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: tasks.length,
      itemBuilder: (context, index) {
        return CheckboxListTile(
          title: Text(tasks[index]),
          value: completed[index],
          onChanged: (value) {
            setState(() {
              completed[index] = value ?? false;
            });
          },
        );
      },
    );
  }
}

30. Dynamic Chat Message List

class Message {
  final String sender;
  final String message;

  Message({
    required this.sender,
    required this.message,
  });
}

final messages = [
  Message(
    sender: 'Amit',
    message: 'Hello!',
  ),
  Message(
    sender: 'Rahul',
    message: 'Hi, how are you?',
  ),
];

ListView.builder(
  itemCount: messages.length,
  itemBuilder: (context, index) {
    final message = messages[index];

    return ListTile(
      leading: const CircleAvatar(
        child: Icon(Icons.person),
      ),
      title: Text(message.sender),
      subtitle: Text(message.message),
    );
  },
)

31. Dynamic Notification List

class NotificationItem {
  final String title;
  final String description;

  NotificationItem({
    required this.title,
    required this.description,
  });
}

final notifications = [
  NotificationItem(
    title: 'New Message',
    description: 'You received a new message.',
  ),
  NotificationItem(
    title: 'Order Updated',
    description: 'Your order has been shipped.',
  ),
];

ListView.builder(
  itemCount: notifications.length,
  itemBuilder: (context, index) {
    final notification = notifications[index];

    return ListTile(
      leading: const Icon(Icons.notifications),
      title: Text(notification.title),
      subtitle: Text(notification.description),
    );
  },
)

32. Pagination and Large Datasets

For extremely large API datasets, do not necessarily download every record at once. A common approach is pagination:

  1. Load the first page.
  2. Display the results.
  3. Detect when the user approaches the bottom.
  4. Request the next page.
  5. Append the new data to the existing list.
  6. Continue until there is no more data.

A ScrollController can be used to observe the scroll position.

final ScrollController controller = ScrollController();

@override
void initState() {
  super.initState();

  controller.addListener(() {
    if (controller.position.pixels >=
        controller.position.maxScrollExtent - 300) {
      loadMoreItems();
    }
  });
}

Remember to dispose of the controller:

@override
void dispose() {
  controller.dispose();
  super.dispose();
}

33. Avoid API Calls Directly Inside build()

Avoid repeatedly starting network requests directly from the build() method. The build method can run many times.

Instead, load data in an appropriate lifecycle method such as initState() or use a suitable state-management/data-fetching architecture.

@override
void initState() {
  super.initState();
  loadProducts();
}

Future loadProducts() async {
  // Fetch data from API
}

34. Dynamic List with FutureBuilder

Future> fetchProducts() async {
  await Future.delayed(
    const Duration(seconds: 2),
  );

  return [
    'Laptop',
    'Phone',
    'Tablet',
    'Headphones',
  ];
}

FutureBuilder>(
  future: fetchProducts(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }

    if (snapshot.hasError) {
      return const Center(
        child: Text('Failed to load products'),
      );
    }

    final products = snapshot.data ?? [];

    if (products.isEmpty) {
      return const Center(
        child: Text('No products found'),
      );
    }

    return ListView.builder(
      itemCount: products.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text(products[index]),
        );
      },
    );
  },
)

35. Reusable Dynamic List Widget

Creating reusable list components can make applications easier to maintain.

class ProductList extends StatelessWidget {
  final List products;

  const ProductList({
    super.key,
    required this.products,
  });

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: products.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text(products[index]),
        );
      },
    );
  }
}

36. List Performance Best Practices

  • Use ListView.builder for large or dynamically generated lists.
  • Provide itemCount whenever the number of items is known.
  • Use ListView.separated when separators are required.
  • Use itemExtent for fixed-size items when appropriate.
  • Use prototypeItem when a representative item can determine a common extent.
  • Use itemExtentBuilder when item extents vary but can be determined efficiently.
  • Avoid unnecessarily creating thousands of widgets before displaying the list.
  • Avoid unnecessary nested scroll views.
  • Use Expanded when a list needs to fill remaining space in a Column.
  • Use shrinkWrap only when the layout actually requires it.
  • Use stable keys for stateful dynamic list items when item identity matters.
  • Keep expensive calculations outside the list item's build path when possible.
  • Do not perform unnecessary network requests inside build().
  • Use pagination for very large remote datasets.
  • Test scrolling performance with realistic amounts of data.

Flutter's performance guidance specifically recommends lazy builder methods for large lists and grids. Flutter Performance Best Practices

37. Common Mistakes

Mistake 1: Using ListView with Thousands of Widgets

ListView(
  children: hugeWidgetList,
)

For large generated datasets, consider a lazy builder instead.

Mistake 2: Forgetting itemCount

ListView.builder(
  itemBuilder: (context, index) {
    return Text(items[index]);
  },
)

When the list length is known, provide itemCount.

Mistake 3: Incorrect Index Access

ListView.builder(
  itemCount: 10,
  itemBuilder: (context, index) {
    return Text(items[index]);
  },
)

If items contains fewer than 10 elements, this can cause a range error. Use itemCount: items.length when displaying the entire list.

Mistake 4: Updating the List Without setState

items.add('New Item');

In a StatefulWidget, update UI state using setState when appropriate:

setState(() {
  items.add('New Item');
});

Mistake 5: Calling Expensive Work for Every Build

Avoid expensive computations, database operations, or network requests directly in frequently executed build methods.

38. ListView.builder vs ListView.separated vs GridView.builder

WidgetUse Case
ListViewSmall or explicitly constructed lists.
ListView.builderLarge or dynamically generated linear lists.
ListView.separatedLists that need dynamically built separators.
GridView.builderLarge or dynamic grid layouts.

39. Complete Dynamic Product List Example

import 'package:flutter/material.dart';

class Product {
  final int id;
  final String name;
  final double price;

  Product({
    required this.id,
    required this.name,
    required this.price,
  });
}

class ProductScreen extends StatefulWidget {
  const ProductScreen({super.key});

  @override
  State createState() => _ProductScreenState();
}

class _ProductScreenState extends State {
  final List products = [
    Product(id: 1, name: 'Laptop', price: 55000),
    Product(id: 2, name: 'Smartphone', price: 30000),
    Product(id: 3, name: 'Tablet', price: 22000),
    Product(id: 4, name: 'Headphones', price: 5000),
  ];

  void addProduct() {
    setState(() {
      products.add(
        Product(
          id: products.length + 1,
          name: 'New Product',
          price: 10000,
        ),
      );
    });
  }

  void removeProduct(int index) {
    setState(() {
      products.removeAt(index);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Dynamic Products'),
        actions: [
          IconButton(
            onPressed: addProduct,
            icon: const Icon(Icons.add),
          ),
        ],
      ),
      body: products.isEmpty
          ? const Center(
              child: Text('No products available'),
            )
          : ListView.builder(
              padding: const EdgeInsets.all(12),
              itemCount: products.length,
              itemBuilder: (context, index) {
                final product = products[index];

                return Card(
                  margin: const EdgeInsets.only(bottom: 10),
                  child: ListTile(
                    leading: CircleAvatar(
                      child: Text('${product.id}'),
                    ),
                    title: Text(product.name),
                    subtitle: Text('₹${product.price}'),
                    trailing: IconButton(
                      icon: const Icon(Icons.delete),
                      onPressed: () {
                        removeProduct(index);
                      },
                    ),
                  ),
                );
              },
            ),
    );
  }
}

40. How Dynamic List Rendering Works

  1. The application stores data in a Dart collection.
  2. ListView.builder receives the collection length through itemCount.
  3. Flutter requests list children through itemBuilder.
  4. The current index identifies which data object should be displayed.
  5. The builder creates the appropriate widget for that object.
  6. When application state changes, the list can be rebuilt with updated data.
  7. For large collections, lazy building avoids constructing the complete widget list upfront.

41. Practical Example: Student List

class Student {
  final String name;
  final int age;
  final String course;

  Student({
    required this.name,
    required this.age,
    required this.course,
  });
}

final students = [
  Student(
    name: 'Aman',
    age: 21,
    course: 'Flutter',
  ),
  Student(
    name: 'Priya',
    age: 22,
    course: 'Dart',
  ),
  Student(
    name: 'Rahul',
    age: 20,
    course: 'UI Development',
  ),
];

ListView.builder(
  itemCount: students.length,
  itemBuilder: (context, index) {
    final student = students[index];

    return Card(
      child: ListTile(
        leading: const CircleAvatar(
          child: Icon(Icons.person),
        ),
        title: Text(student.name),
        subtitle: Text(
          '${student.age} years • ${student.course}',
        ),
      ),
    );
  },
)

42. Practical Example: Dynamic Order List

class Order {
  final String orderId;
  final String product;
  final double amount;
  final String status;

  Order({
    required this.orderId,
    required this.product,
    required this.amount,
    required this.status,
  });
}

final orders = [
  Order(
    orderId: 'ORD001',
    product: 'Laptop',
    amount: 55000,
    status: 'Delivered',
  ),
  Order(
    orderId: 'ORD002',
    product: 'Phone',
    amount: 30000,
    status: 'Pending',
  ),
];

ListView.separated(
  itemCount: orders.length,
  separatorBuilder: (context, index) {
    return const Divider();
  },
  itemBuilder: (context, index) {
    final order = orders[index];

    return ListTile(
      title: Text(order.product),
      subtitle: Text(order.orderId),
      trailing: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('₹${order.amount}'),
          Text(order.status),
        ],
      ),
    );
  },
)

43. When Should You Use ListView.builder?

  • When the number of items is large.
  • When list data is generated dynamically.
  • When data comes from an API or database.
  • When implementing search results.
  • When displaying messages or notifications.
  • When implementing infinite scrolling.
  • When displaying products or orders.
  • When the list can change during runtime.

44. When a Normal ListView May Be Enough

  • The list contains only a few items.
  • The widgets are already explicitly known.
  • The list is small and static.
  • You need to directly provide a small collection of widgets.

Flutter's documentation describes the default ListView constructor as appropriate for small lists and ListView.builder as appropriate for large or infinite lists. Flutter ListView Documentation

45. Quick Revision

ConceptKey Point
Dynamic ListA list whose data can change at runtime.
ListView.builderBuilds list children on demand.
itemBuilderCreates the widget for each index.
itemCountDefines the number of list items.
ListView.separatedBuilds items with separators.
itemExtentDefines a fixed item extent.
prototypeItemProvides a prototype for common item extent.
itemExtentBuilderProvides item extents based on index.
ValueKeyHelps identify items with stable identity.
ScrollControllerControls and observes scrolling.
PaginationLoads large datasets in smaller pages.
shrinkWrapSizes a scroll view to its content when required.

46. Practice Exercises

  1. Create a dynamic list containing 100 student records.
  2. Add an Add Student button.
  3. Add a Delete button for each student.
  4. Implement swipe-to-delete using Dismissible.
  5. Add a search field to filter students.
  6. Display students using ListView.builder.
  7. Create a product list with product name, price, and image.
  8. Implement a checkbox-based shopping list.
  9. Create a notification list with unread/read status.
  10. Build an API-powered list with loading, error, empty, and success states.
  11. Implement pagination for a large API dataset.
  12. Create a horizontal dynamic product list.

47. Key Takeaways

  • Dynamic lists are fundamental to modern Flutter applications.
  • ListView.builder is the primary choice for large and dynamically generated linear lists.
  • Lazy construction helps avoid creating all list-item widgets at once.
  • Always use the correct itemCount when the collection size is known.
  • Keep data separate from UI widgets by using model classes when appropriate.
  • Use ListView.separated when separators are required.
  • Use stable keys when item identity and state preservation matter.
  • Use itemExtent, prototypeItem, or itemExtentBuilder when appropriate to provide child extent information.
  • Handle loading, empty, error, and success states in data-driven lists.
  • For very large remote datasets, combine lazy lists with pagination.
  • Avoid unnecessary nested scrolling and expensive work during builds.

48. Official Flutter Resources

49. Learn Flutter with JustAcademy

For structured Flutter learning, course guidance, and practical training, visit the following resources:

whatsapp